/* eslint no-cond-assign: 0 */

// Remove trailing 'c's. Equivalent to str.replace(/c*$/, '').
// /c*$/ is vulnerable to REDOS.
// invert: Remove suffix of non-c chars instead. Default falsey.
function rtrim(str, c, invert) {
    if (str.length === 0) {
        return '';
    }

    // Length of suffix matching the invert condition.
    var suffLen = 0;

    // Step left until we fail to match the invert condition.
    while (suffLen < str.length) {
        var currChar = str.charAt(str.length - suffLen - 1);
        if (currChar === c && !invert) {
            suffLen++;
        } else if (currChar !== c && invert) {
            suffLen++;
        } else {
            break;
        }
    }

    return str.substr(0, str.length - suffLen);
}

function splitCells(tableRow, count) {
    // ensure that every cell-delimiting pipe has a space
    // before it to distinguish it from an escaped pipe
    var row = tableRow.replace(/\|/g, function (match, offset, str) {
        var escaped = false;
        var curr = offset;
        while (--curr >= 0 && str[curr] === '\\') escaped = !escaped;
        if (escaped) {
            // odd number of slashes means | is escaped
            // so we leave it alone
            return '|';
        } else {
            // add space before unescaped |
            return ' |';
        }
    });
    var cells = row.split(/ \|/);
    var i = 0;

    if (cells.length > count) {
        cells.splice(count);
    } else {
        while (cells.length < count) cells.push('');
    }

    for (; i < cells.length; i++) {
        // leading or trailing whitespace is ignored per the gfm spec
        cells[i] = cells[i].trim().replace(/\\\|/g, '|');
    }
    return cells;
}

const argPluginBlockRule = /^@(\w[a-zA-Z0-9$_]+)\s*?\((.*)\)(\{[\S\s]+\})*[;\S]*/;
const noArgPluginBlockRule = /^@(\w[a-zA-Z0-9$_]+)[;\S]*/;

// custom version of BlockLexer.token
export default function myToken(src, top) {
    src = src.replace(/^ +$/gm, '');
    var next, loose, cap, bull, b, item, space, i, tag, l, isordered, istask, ischecked;

    while (src) {
        // newline
        if ((cap = this.rules.newline.exec(src))) {
            src = src.substring(cap[0].length);
            if (cap[0].length > 1) {
                this.tokens.push({
                    type: 'space',
                });
            }
        }

        // plugin
        if ((cap = argPluginBlockRule.exec(src))) {
            src = src.substring(cap[0].length);
            const args = cap[2].trim(), curToken = {
                type: 'plugin',
                name: cap[1],
                args: args,
                script: null
            }
            if (cap[3]) {
                curToken['script'] = cap[3].trim()
            }
            this.tokens.push(curToken);

            continue;
        }

        // plugin without args
        if ((cap = noArgPluginBlockRule.exec(src))) {
            src = src.substring(cap[0].length);

            this.tokens.push({
                type: 'plugin',
                name: cap[1],
            });

            continue;
        }

        // code
        if ((cap = this.rules.code.exec(src))) {
            src = src.substring(cap[0].length);
            cap = cap[0].replace(/^ {4}/gm, '');
            this.tokens.push({
                type: 'code',
                text: !this.options.pedantic ? rtrim(cap, '\n') : cap,
            });
            continue;
        }

        // fences (gfm)
        if ((cap = this.rules.fences.exec(src))) {
            src = src.substring(cap[0].length);
            this.tokens.push({
                type: 'code',
                lang: cap[2],
                text: cap[3] || '',
            });
            continue;
        }

        // heading
        if ((cap = this.rules.heading.exec(src))) {
            src = src.substring(cap[0].length);
            this.tokens.push({
                type: 'heading',
                depth: cap[1].length,
                text: cap[2],
            });
            continue;
        }

        // table no leading pipe (gfm)
        if (top && (cap = this.rules.nptable.exec(src))) {
            item = {
                type: 'table',
                header: splitCells(cap[1].replace(/^ *| *\| *$/g, '')),
                align: cap[2].replace(/^ *|\| *$/g, '').split(/ *\| */),
                cells: cap[3] ? cap[3].replace(/\n$/, '').split('\n') : [],
            };

            if (item.header.length === item.align.length) {
                src = src.substring(cap[0].length);

                for (i = 0; i < item.align.length; i++) {
                    if (/^ *-+: *$/.test(item.align[i])) {
                        item.align[i] = 'right';
                    } else if (/^ *:-+: *$/.test(item.align[i])) {
                        item.align[i] = 'center';
                    } else if (/^ *:-+ *$/.test(item.align[i])) {
                        item.align[i] = 'left';
                    } else {
                        item.align[i] = null;
                    }
                }

                for (i = 0; i < item.cells.length; i++) {
                    item.cells[i] = splitCells(item.cells[i], item.header.length);
                }

                this.tokens.push(item);

                continue;
            }
        }

        // hr
        if ((cap = this.rules.hr.exec(src))) {
            src = src.substring(cap[0].length);
            this.tokens.push({
                type: 'hr',
            });
            continue;
        }

        // blockquote
        if ((cap = this.rules.blockquote.exec(src))) {
            src = src.substring(cap[0].length);

            this.tokens.push({
                type: 'blockquote_start',
            });

            cap = cap[0].replace(/^ *> ?/gm, '');

            // Pass `top` to keep the current
            // "toplevel" state. This is exactly
            // how markdown.pl works.
            this.token(cap, top);

            this.tokens.push({
                type: 'blockquote_end',
            });

            continue;
        }

        // list
        if ((cap = this.rules.list.exec(src))) {
            src = src.substring(cap[0].length);
            bull = cap[2];
            isordered = bull.length > 1;

            this.tokens.push({
                type: 'list_start',
                ordered: isordered,
                start: isordered ? +bull : '',
            });

            // Get each top-level item.
            cap = cap[0].match(this.rules.item);

            next = false;
            l = cap.length;
            i = 0;

            for (; i < l; i++) {
                item = cap[i];

                // Remove the list item's bullet
                // so it is seen as the next token.
                space = item.length;
                item = item.replace(/^ *([*+-]|\d+\.) +/, '');

                // Outdent whatever the
                // list item contains. Hacky.
                if (~item.indexOf('\n ')) {
                    space -= item.length;
                    item = !this.options.pedantic
                        ? item.replace(new RegExp('^ {1,' + space + '}', 'gm'), '')
                        : item.replace(/^ {1,4}/gm, '');
                }

                // Determine whether the next list item belongs here.
                // Backpedal if it does not belong in this list.
                if (this.options.smartLists && i !== l - 1) {
                    b = this.constructor.rules.bullet.exec(cap[i + 1])[0];
                    if (bull !== b && !(bull.length > 1 && b.length > 1)) {
                        src = cap.slice(i + 1).join('\n') + src;
                        i = l - 1;
                    }
                }

                // Determine whether item is loose or not.
                // Use: /(^|\n)(?! )[^\n]+\n\n(?!\s*$)/
                // for discount behavior.
                loose = next || /\n\n(?!\s*$)/.test(item);
                if (i !== l - 1) {
                    next = item.charAt(item.length - 1) === '\n';
                    if (!loose) loose = next;
                }

                // Check for task list items
                istask = /^\[[ xX]] /.test(item); // !
                ischecked = undefined;
                if (istask) {
                    ischecked = item[1] !== ' ';
                    item = item.replace(/^\[[ xX]] +/, '');
                }

                this.tokens.push({
                    type: loose ? 'loose_item_start' : 'list_item_start',
                    task: istask,
                    checked: ischecked,
                });

                // Recurse.
                this.token(item, false);

                this.tokens.push({
                    type: 'list_item_end',
                });
            }

            this.tokens.push({
                type: 'list_end',
            });

            continue;
        }

        // html
        if ((cap = this.rules.html.exec(src))) {
            src = src.substring(cap[0].length);
            this.tokens.push({
                type: this.options.sanitize ? 'paragraph' : 'html',
                pre: !this.options.sanitizer && (cap[1] === 'pre' || cap[1] === 'script' || cap[1] === 'style'),
                text: cap[0],
            });
            continue;
        }

        // def
        if (top && (cap = this.rules.def.exec(src))) {
            src = src.substring(cap[0].length);
            if (cap[3]) cap[3] = cap[3].substring(1, cap[3].length - 1);
            tag = cap[1].toLowerCase().replace(/\s+/g, ' ');
            if (!this.tokens.links[tag]) {
                this.tokens.links[tag] = {
                    href: cap[2],
                    title: cap[3],
                };
            }
            continue;
        }

        // table (gfm)
        if (top && (cap = this.rules.table.exec(src))) {
            item = {
                type: 'table',
                header: splitCells(cap[1].replace(/^ *| *\| *$/g, '')),
                align: cap[2].replace(/^ *|\| *$/g, '').split(/ *\| */),
                cells: cap[3] ? cap[3].replace(/(?: *\| *)?\n$/, '').split('\n') : [],
            };

            if (item.header.length === item.align.length) {
                src = src.substring(cap[0].length);

                for (i = 0; i < item.align.length; i++) {
                    if (/^ *-+: *$/.test(item.align[i])) {
                        item.align[i] = 'right';
                    } else if (/^ *:-+: *$/.test(item.align[i])) {
                        item.align[i] = 'center';
                    } else if (/^ *:-+ *$/.test(item.align[i])) {
                        item.align[i] = 'left';
                    } else {
                        item.align[i] = null;
                    }
                }

                for (i = 0; i < item.cells.length; i++) {
                    item.cells[i] = splitCells(item.cells[i].replace(/^ *\| *| *\| *$/g, ''), item.header.length);
                }

                this.tokens.push(item);

                continue;
            }
        }

        // lheading
        if ((cap = this.rules.lheading.exec(src))) {
            src = src.substring(cap[0].length);
            this.tokens.push({
                type: 'heading',
                depth: cap[2] === '=' ? 1 : 2,
                text: cap[1],
            });
            continue;
        }

        // top-level paragraph
        if (top && (cap = this.rules.paragraph.exec(src))) {
            src = src.substring(cap[0].length);
            this.tokens.push({
                type: 'paragraph',
                text: cap[1].charAt(cap[1].length - 1) === '\n' ? cap[1].slice(0, -1) : cap[1],
            });
            continue;
        }

        // text
        if ((cap = this.rules.text.exec(src))) {
            // Top-level should never reach here.
            src = src.substring(cap[0].length);
            this.tokens.push({
                type: 'text',
                text: cap[0],
            });
            continue;
        }

        if (src) {
            throw new Error('Infinite loop on byte: ' + src.charCodeAt(0));
        }
    }

    return this.tokens;
}
